<?php

require_once 'PipesImpl/Model/Meta.php';
require_once 'PipesImpl/Model.php';
require_once 'PipesImpl/Namespace.php';
require_once 'Pipes/Exception.php';
require_once 'Pipes/Value/Callback.php';

abstract class Pipes_Model implements PipesImpl_Model
{
	/**
	 *
	 * An array of properties that this Model represents as returned by 
	 * {@link __get()}.
	 *
	 *
	 * @var array
	 *
	 * @access protected
	 */
	private $_data = array();
	
	
	/**
	 *
	 * An array of all of the changes to the various properties
	 *
	 *
	 * @var array
	 *
	 * @access protected
	 *
	 */
	private $_changes = array();
	
	
	/**
	 *
	 * A link to the {@link Domain51_Model_Meta} object that represents this
	 * Model.
	 *
	 *
	 * @var array
	 *
	 * @access protected
	 *
	 */
	private $_meta = null;
	
	
	/**
	 *
	 * A link to a valid callback that will be called on {@link save()}
	 *
	 *
	 * @var mixed
	 *
	 * @access protected
	 *
	 * @see attach()
	 *
	 */
	private $_saveCallback = null;
	
	private $_relationships = array();
	
	/**
	 *
	 * Handle instantiation
	 *
	 * This is called by {@link Domain51_Model_Mapper} when a new Model is
	 * fetched.
	 *
	 * @param array $config An associative array of data that represents this
	 * model.
	 *
	 * @return void
	 *
	 */
	public function __construct(PipesImpl_Model_Meta $meta, $data = array()) {
		$this->_meta = $meta;
		$this->_initData((array)$data);
	}
	
	
	/**
	 *
	 * Return the value of a given key.
	 *
	 * Since Model's keep track of changes without committing to persistant
	 * storage, it is possible for this value to be out of sync with the 
	 * persistant storage layer.  To see if this is a modified value, use
	 * {@link isModified()} and specify the key
	 *
	 * This is a PHP magic method and is called directly by invoking:
	 * <code>$model->key;</code>
	 *
	 * @param string $key
	 *
	 * @return void
	 *
	 */
	public function __get($key) {
		if (isset($this->_changes[$key])) {
			// return latest changed version of this
			return $this->_changes[$key];
		} elseif (isset($this->_data[$key])) {
			// return property as set at construct
			return $this->_data[$key];
		}
		
		// look for cached relationships
		if (isset($this->_relationships[$key])) {
			return $this->_relationships[$key];
		}
		
		// look for relationships that haven't been cached
		if ($this->_meta->hasRelationship($key)) {
			// strip "s" off end
			$realKey = substr($key, 0, -1);
			$this->_relationships[$key] = $this->_meta->getRelationship($this, $realKey);
			return $this->_relationships[$key];
		}
		
		// nothing returned
		return null;
	}
	
	
	/**
	 *
	 * Sets a given property to a specific value.
	 *
	 * Throws an exception when the specified $key is unknown.
	 *
	 * This is a PHP magic method, and is called directly by invoking:
	 * <code>$model->key = "value";</code>
	 *
	 * In addition to known specific keys, if $key is the name of a relationship
	 * that this Model has, it will be set as well.  It is assumed that any 
	 * implementing model that uses this style of setting will implement the 
	 * handling of this value by overriding {@link save()}.
	 *
	 *
	 * @param string $key
	 *
	 * @param mixed $value
	 *
	 * @return void
	 *
	 */
	public function __set($key, $value) {
		$knownColumns = $this->getColumns();
		if (!isset($knownColumns[$key]) && !$this->_meta->hasRelationship($key)) {
			throw new Pipes_Exception("ERR_UNKNOWN_PROPERTY [$key]");
		}
		$this->_changes[$key] = $value;
	}
	
	
	/**
	 *
	 * Returns true if a given property is set
	 *
	 * This is a PHP magic method and is called directly by invoking:
	 * <code>isset($model->key);</code>
	 *
	 *
	 * @param string $key
	 *
	 * @return bool
	 *
	 */
	public function __isset($key) {
		return isset($this->_data[$key]) || isset($this->_changes[$key]);
	}
	
	
	/**
	 *
	 * Unset a changed property
	 *
	 * This will not unset properties from persistent storages.  To change the
	 * type, you will need to actually set it to the type you wish to use.
	 *
	 * This is a PHP magic method and is called directly by invoking:
	 * <code>unset($model->key);</code>
	 *
	 *
	 * @param string $key
	 *
	 * @return void
	 *
	 */
	public function __unset($key) {
		unset($this->_changes[$key]);
	}
	
	/**
	 *
	 * Revert all changes to this property
	 *
	 *
	 * @return void
	 *
	 */
	public function revert() {
		$this->_changes = array();
	}
	
	/**
	 *
	 * Returns an array of the latest data
	 *
	 *
	 * @return array
	 *
	 */
	public function toArray() {
		return array_merge($this->_data, $this->_changes);
	}
	
	
	/**
	 *
	 * Return true if this Model has been modified.  If $key is provided, 
	 * returns true only if that property has been modified.
	 *
	 *
	 * @param string|null $key Specify which property to check if it is 
	 *	modified.  Optional.
	 *
	 * @return bool
	 *
	 */
	public function isModified($key = null) {
		if (empty($key)) {
			return (count($this->_changes) > 0);
		} else {
			return isset($this->_changes[$key]);
		}
	}
	
	
	/**
	 *
	 * Return true if this Model is new
	 *
	 *
	 * @return bool
	 *
	 */
	public function isNew() {
		return (count($this->_data) == 0);
	}
	
	
	/**
	 *
	 * Attaches a valid PHP callback to be invoked on {@link save()}
	 *
	 * Only one callback can be attached at a time.  See 
	 * {@link http://www.php.net/manual/en/function.is-callable.php is_callable()}
	 * for a definition of what a valid callback is.
	 *
	 *
	 * @param mixed $callback A valid PHP callback.
	 *
	 * @return void
	 *
	 */
	public function attach($callback) {
		$this->_saveCallback = new Pipes_Value_Callback($callback);
	}
	
	
	/**
	 *
	 * Saves this Model.
	 *
	 * If a callback has been specified by {@link attach()}, that callback is
	 * invoked with this object as the only parameter.
	 *
	 * On save(), all changed data is persisted, and unset() will no longer 
	 * revert changes.
	 *
	 *
	 * @return void
	 *
	 */
	public function save() {
		if (!is_null($this->_saveCallback) && $this->isModified()) {
			$updateChanges = (array)call_user_func($this->_saveCallback->value, $this);
			$this->_changes = array_merge($this->_changes, $updateChanges);
		}

		// always call save() on any loaded relationships and let them determine
		// if they should do anything
		foreach ($this->_relationships as $relationship) {
			//echo "Saving " .get_class($relationship) . "<br />";
			$relationship->save();
		}

		
		$this->_data = array_merge($this->_data, $this->_changes);
		$this->_changes = array();
	}
	
	public function getColumns() {
		return $this->_meta->getColumns();
	}

	public function listRelationships() {
		return $this->_meta->listRelationships();
	}
	
	public function getName() {
		$exploded = explode('_', get_class($this));
		return strtolower(array_pop($exploded)); 
	}
	
	public function idColumn() {
		$columns = $this->getColumns();
		return $columns[$this->getName() . '_id'];
	}
	
	public function accept(PipesImpl_Visitor $visitor) {
		return $visitor->visit($this);
	}

	protected function _initData(array $data = array()) {
		if (count($data) <= 0) {
			return;
		}
		
		// populate _data based on meta object
		$columnData = $this->getColumns();
		$columnNames = array_keys($columnData);
		foreach ($columnNames as $value) {
			if (isset($data[$value])) {
				$this->_data[$value] = $data[$value];
			}
		}
	}
}

